/*
 * Copyright 2014 Mario Guggenberger <mg@protyposis.net>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.protyposis.ohos.mediaplayer;

import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.DataAbilityRemoteException;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.surfaceprovider.SurfaceProvider;
import ohos.agp.graphics.Surface;
import ohos.agp.graphics.SurfaceOps;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.media.common.Source;
import ohos.media.player.Player;
import ohos.utils.net.Uri;

import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Created by maguggen on 04.06.2014.
 * <p>
 * MediaController.MediaPlayerControl  暂未找到对应的api
 * 使用接口实现对视频播放的控制
 * AVController.PlayControls  也可以控制视频播放
 * AVController.getPlayControls  可以管理播放的操作
 * Player也可以控制视频的播放
 */
public class VideoView extends SurfaceProvider implements SurfaceOps.Callback, Component.EstimateSizeListener,
        Player.IPlayerCallback, MediaPlayerControl {

    private static final String TAG = VideoView.class.getSimpleName();

    public static final int STATE_ERROR = -1;
    public static final int STATE_IDLE = 0;
    public static final int STATE_PREPARING = 1;
    public static final int STATE_BUFFERING = 2;
    public static final int STATE_PREPARED = 3;
    public static final int STATE_PLAYING = 4;
    public static final int STATE_PAUSED = 5;
    public static final int STATE_PLAYBACK_COMPLETED = 6;
    public static final int STATE_STOPPED = 7;

    private int mCurrentState = STATE_IDLE;
    private int mTargetState = STATE_IDLE;

    private Source mSource;
    private Player mPlayer;
    private SurfaceOps mSurfaceHolder;
    private int mVideoWidth;
    private int mVideoHeight;
    private int mSeekWhenPrepared;
    private float mPlaybackSpeedWhenPrepared;
    private int mBufferPercent;

    private OnPreparedListener mOnPreparedListener;
    private OnSeekListener mOnSeekListener;
    private OnSeekCompleteListener mOnSeekCompleteListener;
    private OnCompletionListener mOnCompletionListener;
    private OnErrorListener mOnErrorListener;
    private OnInfoListener mOnInfoListener;
    private OnBufferingUpdateListener mOnBufferingUpdateListener;
    private List<StatusChangeListener> statusChangeListener;
    private Context mContext;

    private EventHandler handler;

    public VideoView(Context context) {
        super(context);
        mContext = context;
        initVideoView();
    }

    public VideoView(Context context, AttrSet attrs) {
        super(context, attrs);
        mContext = context;
        initVideoView();
    }

    public VideoView(Context context, AttrSet attrs, int defStyle) {
        super(context, attrs);
        initVideoView();
    }

    private void initVideoView() {
        if (mSurfaceHolder == null) {
            mSurfaceHolder = this.getSurfaceOps().get();
        }
        mSurfaceHolder.addCallback(this);
//        setEstimateSizeListener(this);
        handler = new EventHandler(EventRunner.getMainEventRunner());
    }

    /**
     * Sets a media source and track indices.
     * for a detailed explanation of the parameters.
     *
     * @param source the media source
     */
    public void setVideoSource(Source source) {
        mCurrentState = STATE_IDLE;
        mTargetState = STATE_IDLE;

        onStatusChange();

        mSource = source;
        mSeekWhenPrepared = 0;
        mPlaybackSpeedWhenPrepared = 1;
        openVideo();
    }

    /**
     * @param path
     */
    @Deprecated
    public void setVideoPath(String path) {
        setVideoSource(new Source(path));
    }

    /**
     * @param uri
     */
    @Deprecated
    public void setVideoURI(Uri uri) {
        try {
            DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
            FileDescriptor fd = helper.openFile(uri, "r");
            Source source = new Source(fd);
            setVideoSource(source);
        } catch (DataAbilityRemoteException | FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param uri
     * @param headers
     */
    @Deprecated
    public void setVideoURI(Uri uri, Map<String, String> headers) {
        setVideoSource(new Source(uri.toString(), headers));
    }

    /**
     * getMediaPlayer
     * @return Player
     */
    public Player getMediaPlayer() {
        // TODO do not return the real media player
        // Handling width it could result in invalid states, better return a "censored" wrapper interface
        return mPlayer;
    }

    private void runOnUi(Runnable runnable) {
        handler.postTask(runnable);
    }

    private void onStatusChange() {
        if (statusChangeListener != null) {
            statusChangeListener.forEach(l -> l.onStatusChanged(mCurrentState));
        }
    }

    private void openVideo() {
        if (mSource == null || mSurfaceHolder == null || mSurfaceHolder.getSurface() == null) {
            // not ready for playback yet, will be called again later
            return;
        }

        release();

        mPlayer = new Player(getContext());
        mPlayer.setPlayerCallback(this);

        // Copy the player reference for use in the thread which allows us to later compare
        // references and detect player changes (release, reload).
        final Player currentPlayer = mPlayer;

        // Set the data source asynchronously as this might take a while, e.g. if data has to be
        // requested from the network/internet.
        // IMPORTANT:
        // We use a Thread instead of an AsyncTask for performance reasons, because threads started
        // in an AsyncTask perform much worse, no matter the priority the Thread gets (unless the
        // AsyncTask's priority is elevated before creating the Thread).
        // See comment in MediaPlayer#prepareAsync for detailed explanation.
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                try {
        mCurrentState = STATE_PREPARING;
        onStatusChange();

        new Thread(() -> {
            currentPlayer.setSource(mSource);
            currentPlayer.setVideoSurface(mSurfaceHolder.getSurface());


            // Async prepare spawns another thread inside this thread which really isn't
            // necessary; we call this method anyway because of the events it triggers
            // when it fails, and to stay in sync which the VideoView that does
            // the same.
            currentPlayer.prepare();
            LogUtil.loge(TAG, "video opened");
        }).start();
//                } catch (Exception e) {
//                    LogUtil.loge(TAG, "something went wrong" + e);
//                }
//            }
//        }).start();
    }

    private void updateVideoSize() {
        int newWidth = getWidth();
        int newHeight = getHeight();
        double videoScale = (double) mPlayer.getVideoWidth() / mPlayer.getVideoHeight();
        if (videoScale > 0 && (newWidth != mVideoWidth || newHeight != mVideoHeight)) {
            mVideoWidth = newWidth;
            mVideoHeight = newHeight;
            runOnUi(() -> {
                if (videoScale > 1) {
//                    setWidth(mVideoWidth);
//                    setHeight((int) Math.min(mVideoWidth / videoScale, mVideoHeight));
//                    setHeight((int) (mVideoWidth / videoScale));
                    int h = (int) (mVideoWidth / videoScale);
                    if (h > mVideoHeight) {
                        setWidth((int) (mVideoHeight * videoScale));
                        setHeight(mVideoHeight);
                    } else {
                        setWidth(mVideoWidth);
                        setHeight(h);
                    }
                } else {
                    int w = (int) (mVideoHeight * videoScale);
                    if (w > mVideoWidth) {
                        setWidth(mVideoWidth);
                        setHeight((int) (mVideoWidth / videoScale));
                    } else {
                        setHeight(mVideoHeight);
                        setWidth((int) (mVideoHeight * videoScale));
                    }
                }
            });
        }
    }

    /**
     * Resizes the video view according to the video size to keep aspect ratio.
     */
    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {

//        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
//        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);

        int width = EstimateHelper.getDefaultSize(EstimateSpec.getSize(widthMeasureSpec), widthMeasureSpec);
        int height = EstimateHelper.getDefaultSize(EstimateSpec.getSize(heightMeasureSpec), heightMeasureSpec);

        if (mVideoWidth > 0 && mVideoHeight > 0) {

            int widthSpecMode = EstimateSpec.getMode(widthMeasureSpec);
            int widthSpecSize = EstimateSpec.getSize(widthMeasureSpec);
            int heightSpecMode = EstimateSpec.getMode(heightMeasureSpec);
            int heightSpecSize = EstimateSpec.getSize(heightMeasureSpec);

            if (widthSpecMode == EstimateSpec.PRECISE && heightSpecMode == EstimateSpec.PRECISE) {
                // the size is fixed
                width = widthSpecSize;
                height = heightSpecSize;

                // for compatibility, we adjust size based on aspect ratio
                if (mVideoWidth * height < width * mVideoHeight) {
                    //Log.i("@@@", "image too wide, correcting");
                    width = height * mVideoWidth / mVideoHeight;
                } else if (mVideoWidth * height > width * mVideoHeight) {
                    //Log.i("@@@", "image too tall, correcting");
                    height = width * mVideoHeight / mVideoWidth;
                }
            } else if (widthSpecMode == EstimateSpec.PRECISE) {
                // only the width is fixed, adjust the height to match aspect ratio if possible
                width = widthSpecSize;
                height = width * mVideoHeight / mVideoWidth;
                if (heightSpecMode == EstimateSpec.NOT_EXCEED && height > heightSpecSize) {
                    // couldn't match aspect ratio within the constraints
                    height = heightSpecSize;
                }
            } else if (heightSpecMode == EstimateSpec.PRECISE) {
                // only the height is fixed, adjust the width to match aspect ratio if possible
                height = heightSpecSize;
                width = height * mVideoWidth / mVideoHeight;
                if (widthSpecMode == EstimateSpec.NOT_EXCEED && width > widthSpecSize) {
                    // couldn't match aspect ratio within the constraints
                    width = widthSpecSize;
                }
            } else {
                // neither the width nor the height are fixed, try to use actual video size
                width = mVideoWidth;
                height = mVideoHeight;
                if (heightSpecMode == EstimateSpec.NOT_EXCEED && height > heightSpecSize) {
                    // too tall, decrease both width and height
                    height = heightSpecSize;
                    width = height * mVideoWidth / mVideoHeight;
                }
                if (widthSpecMode == EstimateSpec.NOT_EXCEED && width > widthSpecSize) {
                    // too wide, decrease both width and height
                    width = widthSpecSize;
                    height = width * mVideoHeight / mVideoWidth;
                }
            }
        } else {
            // no size yet, just adopt the given spec sizes
        }

        setEstimatedSize(width, height);

        return true;
    }

    /**
     * release
     */
    public void release() {
        if (mPlayer != null) {
            mPlayer.release();
            mPlayer = null;
        }
        mCurrentState = STATE_IDLE;
        mTargetState = STATE_IDLE;
        onStatusChange();
    }

    /**
     * setOnPreparedListener
     * @param l l
     */
    public void setOnPreparedListener(OnPreparedListener l) {
        this.mOnPreparedListener = l;
    }

    /**
     * setOnSeekListener
     * @param l l
     */
    public void setOnSeekListener(OnSeekListener l) {
        this.mOnSeekListener = l;
    }

    /**
     * setOnSeekCompleteListener
     * @param l l
     */
    public void setOnSeekCompleteListener(OnSeekCompleteListener l) {
        this.mOnSeekCompleteListener = l;
    }

    /**
     * setOnCompletionListener
     * @param l l
     */
    public void setOnCompletionListener(OnCompletionListener l) {
        this.mOnCompletionListener = l;
    }

    /**
     * setOnBufferingUpdateListener
     * @param l l
     */
    public void setOnBufferingUpdateListener(OnBufferingUpdateListener l) {
        this.mOnBufferingUpdateListener = l;
    }

    /**
     * setOnErrorListener
     * @param l l
     */
    public void setOnErrorListener(OnErrorListener l) {
        this.mOnErrorListener = l;
    }

    /**
     * setOnInfoListener
     * @param l l
     */
    public void setOnInfoListener(OnInfoListener l) {
        this.mOnInfoListener = l;
    }

    @Override
    public int getStatus() {
        return mCurrentState;
    }

    /**
     * start
     */
    public void start() {
        if (isInPlaybackState()) {
            mPlayer.play();
            mCurrentState = STATE_PLAYING;
            onStatusChange();
        } else {
            mTargetState = STATE_PLAYING;
        }
    }

    /**
     * pause
     */
    public void pause() {
        if (isInPlaybackState()) {
            mPlayer.pause();
            mCurrentState = STATE_PAUSED;
            onStatusChange();
        }
        mTargetState = STATE_PAUSED;
    }

    /**
     * stopPlayback
     */
    public void stopPlayback() {
        if (mPlayer != null) {
            mPlayer.stop();
            mCurrentState = STATE_STOPPED;
            mTargetState = STATE_STOPPED;

            onStatusChange();
        }
    }

    /**
     * Sets the playback speed. Can be used for fast forward and slow motion.
     * The speed must not be negative but can otherwise be set to any value. The player will not
     * skip frames though and only playback at the maximum speed that the device can decode and
     * process (setting 10x speed thus will not lead to an actual 10x speedup).
     * <p>
     * speed 0.5 = half speed / slow motion
     * speed 2.0 = double speed / fast forward
     * speed 0.0 equals to pause
     *
     * @param speed the playback speed to set
     * @throws IllegalArgumentException if the speed is negative
     */
    public void setPlaybackSpeed(float speed) {
        if (speed < 0) {
            throw new IllegalArgumentException("speed cannot be negative");
        }

        if (isInPlaybackState()) {
            mPlayer.setPlaybackSpeed(speed);
        }
        mPlaybackSpeedWhenPrepared = speed;
    }

    /**
     * Gets the current playback speed. See {@link #setPlaybackSpeed(float)} for details.
     *
     * @return the current playback speed
     */
    public float getPlaybackSpeed() {
        if (isInPlaybackState()) {
            return mPlayer.getPlaybackSpeed();
        } else {
            return mPlaybackSpeedWhenPrepared;
        }
    }

    /**
     * getDuration
     * @return int
     */
    public int getDuration() {
        return mPlayer != null ? mPlayer.getDuration() : 0;
    }

    /**
     * getCurrentPosition
     * @return int
     */
    public int getCurrentPosition() {
        if (isInPlaybackState()) {
            return mPlayer.getCurrentTime();
        }
        return 0;
    }

    /**
     * seekTo
     * @param msec msec
     */
    public void seekTo(int msec) {
        if (mOnSeekListener != null) {
            mOnSeekListener.onSeek(mPlayer);
        }

        if (isInPlaybackState()) {
            mPlayer.rewindTo(msec * 1000);
            mSeekWhenPrepared = 0;
        } else {
            mSeekWhenPrepared = msec;
        }
    }

    @Override
    public boolean isPlaying() {
        return mPlayer != null && mPlayer.isNowPlaying();
    }

    @Override
    public int getBufferPercentage() {
        return mBufferPercent;
    }

    @Override
    public boolean canPause() {
        return true;
    }

    @Override
    public boolean canSeekBackward() {
        return true;
    }

    @Override
    public boolean canSeekForward() {
        return true;
    }

    @Override
    public void addStatusChangeListener(StatusChangeListener listener) {
        if (statusChangeListener == null) statusChangeListener = new ArrayList<>();
        statusChangeListener.add(listener);
    }

//    public MediaPlayer.SeekMode getSeekMode() {
//        return mPlayer.getSeekMode();
//    }

//    public void setSeekMode(MediaPlayer.SeekMode seekMode) {
//        mPlayer.setSeekMode(seekMode);
//    }

    private boolean isInPlaybackState() {
        return mPlayer != null && mCurrentState >= STATE_PREPARED;
    }

    /**
     * getSurface
     * @return Surface
     */
    public Surface getSurface() {
        return mSurfaceHolder.getSurface();
    }

    @Override
    public void surfaceCreated(SurfaceOps surfaceOps) {
        mSurfaceHolder = surfaceOps;
        if (mCurrentState <= STATE_IDLE) openVideo();
    }

    @Override
    public void surfaceChanged(SurfaceOps surfaceOps, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(SurfaceOps surfaceOps) {
        mSurfaceHolder = null;
        release();
    }

    @Override
    public void onPrepared() {
        mCurrentState = STATE_PREPARED;

        onStatusChange();
        updateVideoSize();
        setPlaybackSpeed(mPlaybackSpeedWhenPrepared);

        if (mOnPreparedListener != null) {
            runOnUi(() -> mOnPreparedListener.onPrepared(mPlayer));
        }

        int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
        if (seekToPosition != 0) {
            seekTo(seekToPosition);
        }

        if (mTargetState == STATE_PLAYING) {
            start();
        }

        if (mCurrentState != STATE_PLAYING && mPlayer.isNowPlaying()) {
            mPlayer.pause();
//            mPlayer.rewindTo(10000);
        }
    }

    @Override
    public void onMessage(int type, int extra) {

//        if (extra == 0) {
//            switch (type) {
//                case Player.PLAYER_INFO_BUFFERING_START:
//                    mCurrentState = STATE_BUFFERING;
//                    onStatusChange();
//                    break;
//                case Player.PLAYER_INFO_BUFFERING_END:
//
//                    break;
//                default:
//                    break;
//            }
//        }

        if (mOnInfoListener != null) {
            runOnUi(() -> mOnInfoListener.onInfo(mPlayer, type, extra));
        }
    }

    @Override
    public void onError(int errorType, int errorCode) {
        mCurrentState = STATE_ERROR;
        mTargetState = STATE_ERROR;

        onStatusChange();

        if (mOnErrorListener != null) {
            runOnUi(() -> mOnErrorListener.onError(mPlayer, errorType, errorCode));
        }
    }

    @Override
    public void onResolutionChanged(int width, int height) {
        updateVideoSize();
    }

    @Override
    public void onPlayBackComplete() {
        mCurrentState = STATE_PLAYBACK_COMPLETED;
        mTargetState = STATE_PLAYBACK_COMPLETED;
        if (mOnCompletionListener != null) {
            runOnUi(() -> mOnCompletionListener.onCompletion(mPlayer));
        }

        onStatusChange();
    }

    @Override
    public void onRewindToComplete() {
        if (mOnSeekCompleteListener != null) {
            runOnUi(() -> mOnSeekCompleteListener.onSeekComplete(mPlayer));
        }
    }

    @Override
    public void onBufferingChange(int percent) {
        mBufferPercent = percent;
        if (mOnBufferingUpdateListener != null) {
            runOnUi(() -> mOnBufferingUpdateListener.onBufferingUpdate(mPlayer, percent));
        }
    }

    @Override
    public void onNewTimedMetaData(Player.MediaTimedMetaData mediaTimedMetaData) {

    }

    @Override
    public void onMediaTimeIncontinuity(Player.MediaTimeInfo mediaTimeInfo) {

    }

    /**
     * Interface definition for a callback to be invoked when the media
     * source is ready for playback.
     */
    public interface OnPreparedListener {
        /**
         * Called when the media file is ready for playback.
         *
         * @param mp the MediaPlayer that is ready for playback
         */
        void onPrepared(Player mp);
    }

    /**
     * Interface definition of a callback to be invoked when a seek
     * is issued.
     */
    public interface OnSeekListener {
        /**
         * Called to indicate that a seek operation has been started.
         *
         * @param mp the mediaPlayer that the seek was called on
         */
        public void onSeek(Player mp);
    }

    /**
     * Interface definition of a callback to be invoked indicating
     * the completion of a seek operation.
     */
    public interface OnSeekCompleteListener {
        /**
         * Called to indicate the completion of a seek operation.
         *
         * @param mp the MediaPlayer that issued the seek operation
         */
        public void onSeekComplete(Player mp);
    }

    public interface OnVideoSizeChangedListener {
        /**
         * Called to indicate the video size
         * <p>
         * The video size (width and height) could be 0 if there was no video,
         * no display surface was set, or the value was not determined yet.
         *
         * @param mp     the MediaPlayer associated with this callback
         * @param width  the width of the video
         * @param height the height of the video
         */
        public void onVideoSizeChanged(Player mp, int width, int height);
    }

    /**
     * Interface definition of a callback to be invoked indicating buffering
     * status of a media resource being streamed over the network.
     */
    public interface OnBufferingUpdateListener {
        /**
         * Called to update status in buffering a media stream received through
         * progressive HTTP download. The received buffering percentage
         * indicates how much of the content has been buffered or played.
         * For example a buffering update of 80 percent when half the content
         * has already been played indicates that the next 30 percent of the
         * content to play has been buffered.
         *
         * @param mp      the MediaPlayer the update pertains to
         * @param percent the percentage (0-100) of the content
         *                that has been buffered or played thus far
         */
        void onBufferingUpdate(Player mp, int percent);
    }

    /**
     * Interface definition for a callback to be invoked when playback of
     * a media source has completed.
     */
    public interface OnCompletionListener {
        /**
         * Called when the end of a media source is reached during playback.
         *
         * @param mp the MediaPlayer that reached the end of the file
         */
        void onCompletion(Player mp);
    }

    /**
     * Interface definition of a callback to be invoked when there
     * has been an error during an asynchronous operation (other errors
     * will throw exceptions at method call time).
     */
    public interface OnErrorListener {
        /**
         * Called to indicate an error.
         *
         * @param mp    the MediaPlayer the error pertains to
         * @param what  the type of error that has occurred:
         *              <ul>
         *              <li>{@link Player#PLAYER_ERROR_UNKNOWN}
         *              <li>{@link Player#PLAYER_ERROR_SERVER_DIED}
         *              </ul>
         * @param extra an extra code, specific to the error. Typically
         *              implementation dependent.
         *              <ul>
         *              <li>{@link Player#PLAYER_ERROR_IO}
         *              <li>{@link Player#PLAYER_ERROR_MALFORMED}
         *              <li>{@link Player#PLAYER_ERROR_UNSUPPORTED}
         *              <li>{@link Player#PLAYER_ERROR_TIMED_OUT}
         *              </ul>
         * @return True if the method handled the error, false if it didn't.
         * Returning false, or not having an OnErrorListener at all, will
         * cause the OnCompletionListener to be called.
         */
        boolean onError(Player mp, int what, int extra);
    }

    /**
     * Interface definition of a callback to be invoked to communicate some
     * info and/or warning about the media or its playback.
     */
    public interface OnInfoListener {
        /**
         * Called to indicate an info or a warning.
         *
         * @param mp    the MediaPlayer the info pertains to.
         * @param what  the type of info or warning.
         *              <ul>
         *              <li>{@link Player#PLAYER_INFO_UNKNOWN}
         *              <li>{@link Player#PLAYER_INFO_VIDEO_TRACK_LAGGING}
         *              <li>{@link Player#PLAYER_INFO_VIDEO_RENDERING_START}
         *              <li>{@link Player#PLAYER_INFO_BUFFERING_START}
         *              <li>{@link Player#PLAYER_INFO_BUFFERING_END}
         *              <li>{@link Player#PLAYER_INFO_NETWORK_BANDWIDTH}
         *              <li>{@link Player#PLAYER_INFO_BAD_INTERLEAVING}
         *              <li>{@link Player#PLAYER_INFO_NOT_SEEKABLE}
         *              <li>{@link Player#PLAYER_INFO_METADATA_UPDATE}
         *              <li>{@link Player#PLAYER_INFO_UNSUPPORTED_SUBTITLE}
         *              <li>{@link Player#PLAYER_INFO_SUBTITLE_TIMED_OUT}
         *              </ul>
         * @param extra an extra code, specific to the info. Typically
         *              implementation dependent.
         * @return True if the method handled the info, false if it didn't.
         * Returning false, or not having an OnErrorListener at all, will
         * cause the info to be discarded.
         */
        boolean onInfo(Player mp, int what, int extra);
    }
}
